菜品管理
1.1 公共字段自动填充
在很多表中有很多重复字段,比如创建人id,创建时间,更新人id,更新时间等,让我们的项目代码冗余,不易维护,公共字段自动填充技术就是来解决这一问题的。

实现思路
- 对于create_time,create_user两个字段,是在进行插入操作时修改的,update_time,update_user两个字段是在插入和修改两个操作中修改的
- 我们可以选择AOP的方法,通过自定义注解
@AutoFill来指定增强范围,通过AOP的连接点参数,使用反射的方法给字段赋值 - 为什么不用切入点表达式?因为不太好确定范围,使用正则表达式的话,需要限制方法名,不灵活
代码实现
导入依赖
java<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>自定义注解
java/** * 自定义注解,用于标识哪些方法需要增强公共字段填充处理 */ @Target(ElementType.METHOD) // 作用在方法上 @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { /** * 指定数据库操作类型 * @return */ OperationType value(); }切入点表达式
java/** * 切入点 */ @Pointcut("execution(* com.sky.mapper.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointcut() {}通知
java/** * 前置通知(增强逻辑),在通知中进行公共字段的赋值 */ @Before("autoFillPointcut()") public void autoFill(JoinPoint joinPoint) { log.info("开始进行公共字段的填充..."); // 1. 获取被拦截方法的@AutoFill注解的OperationType值 MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 获得签名对象 AutoFill annotation = signature.getMethod().getAnnotation(AutoFill.class);// 获得方法上的注解对象 OperationType value = annotation.value();// 获得数据库操作类型 // 2. 获取到被拦截方法的参数(需要被赋值的实体类) // 这里做一个约定,要想实现自动填充的话,将被增强参数放在第一个位置 Object[] args = joinPoint.getArgs(); if (args == null || args.length == 0) { log.debug("AOP增强方法参数有问题!{}",args); return; } Object entity = args[0]; // 3. 准备赋值的数据 LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId(); // 4. 根据OperationType值不同,选择不同的增强逻辑,为实体类通过反射赋值 if(value == OperationType.INSERT){ // 为四个字段赋值 try { Method setCreateTime = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setUpdateTime = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateUser = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setUpdateUser.invoke(entity, currentId); setCreateUser.invoke(entity, currentId); setUpdateTime.invoke(entity, now); setCreateTime.invoke(entity, now); } catch (Exception e) { throw new RuntimeException(e); } } else if (value == OperationType.UPDATE) { // 为两个字段赋值 try { Method setUpdateTime = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setUpdateUser.invoke(entity, currentId); setUpdateTime.invoke(entity, now); } catch (Exception e) { throw new RuntimeException(e); } } }
1.2 新增菜品
1.2.1 需求分析和设计
产品原型

业务规则
- 菜品名称唯一
- 菜品必须属于某个分类下,不能单独存在(外键约束以及逻辑外键约束(推荐)、删除分类时需要先将菜品删除完)
- 新增菜品可以根据情况选择菜品的口味
- 每个菜品必须对应一张图片
1.2.2 代码实现之-上传文件(阿里云OSS)
添加依赖
xml<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.17.4</version> </dependency>如果使用的是Java 9及以上的版本,则需要添加以下JAXB相关依赖。
xml<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!-- no more than 2.3.3--> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.3</version> </dependency>定义配置属性类,这样该属性类可以直接拿到配置文件中的属性
java@Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }定义OSS工具类
java@Data @AllArgsConstructor @Slf4j public class AliOssUtil { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; /** * 文件上传 * * @param bytes * @param objectName * @return */ public String upload(byte[] bytes, String objectName) { // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); try { // 创建PutObject请求。 ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes)); } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason."); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network."); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null) { ossClient.shutdown(); } } //文件访问路径规则 https://BucketName.Endpoint/ObjectName StringBuilder stringBuilder = new StringBuilder("https://"); stringBuilder .append(bucketName) .append(".") .append(endpoint) .append("/") .append(objectName); log.info("文件上传到:{}", stringBuilder.toString()); return stringBuilder.toString(); } }定义配置类
java/** * 创建一个AliOssUtil类对象交给SpringIOC容器管理 */ @Slf4j @Configuration public class OssConfiguration { @Bean @ConditionalOnMissingBean public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) { return new AliOssUtil(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } }@ConditionalOnMissingBean,对象是单例模式,容器中同一时刻只能存在一个对象。
注意:在后面的开发中遇到了问题,我修改了AliyunOSSUtils类的endpoint属性,导致我连续上传文件时,解析路径时数组越界,所以对于这种由配置类声明的工具类,不要随便改属性,会影响容器中的那个对象,是全局生效的。
写Controller
java/** * 利用阿里云OSS服务完成文件的上传 * @param file * @return */ @PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { log.info("文件上传:{}", file.getOriginalFilename()); // 在Bucket上的储存路径 // 目录 String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); // 文件名 : UUID + 文件后缀名 String newFileName = UUID.randomUUID() + file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")); String objectName = dir + "/" + newFileName; try { String upload = aliOssUtil.upload(file.getBytes(), objectName); return Result.success(upload); } catch (IOException e) { log.error("文件上传失败,{}",e); } return Result.error("文件上传失败!"); } }需要在yml配置文件中写入参数
ymlsky: alioss: endpoint: ${sky.alioss.endpoint} bucket-name: ${sky.alioss.bucket-name} access-key-id: ${sky.alioss.access-key-id} access-key-secret: ${sky.alioss.access-key-secret}这里使用了引用的方式赋值,在不同的环境下可以配置不同的OSS服务
1.2.3 主键回填
在 MyBatis 中,主键回填(Generated Keys) 是指在执行 INSERT 语句后,由数据库自动生成的自增主键(如 MySQL 的 AUTO_INCREMENT)被自动赋值回到 Java 实体对象的过程。
这在你的“苍穹外卖”项目中至关重要,因为你需要先插入菜品(Dish),拿到生成的 ID 后,才能给口味表(DishFlavor)设置关联的 dish_id。
- 如何在 XML 中开启主键回填?
你需要在 <insert> 标签中添加两个关键属性:
useGeneratedKeys="true":告诉 MyBatis 使用数据库内置的生成机制获取主键。keyProperty="id":指定将获取到的主键值赋值给传入对象的哪一个属性(通常是实体类中的id字段)。
XML 配置示例:
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
</insert>- 在 Java 代码中如何获取?
执行完 Mapper 方法后,你不需要去调用类似 getGeneratedId 的方法。MyBatis 会直接修改你传入的那个对象。
@Override
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
// 1. 执行插入操作
// 此时 dish.getId() 为 null
dishMapper.save(dish);
// 2. 核心时刻:执行完上面那行后,MyBatis 已经把 ID 填进去了
Long newDishId = dish.getId();
log.info("生成的新菜品 ID 为:{}", newDishId);
// 3. 后续业务:给口味关联这个 ID
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && !flavors.isEmpty()) {
flavors.forEach(f -> f.setDishId(newDishId));
dishFlavorMapper.insertBatch(flavors);
}
}注意事项与常见错误
返回值的误解:
Mapper 方法的返回值
int依然是受影响的行数(通常是 1),而不是主键 ID。ID 是通过对象的getter方法拿到的。XML 属性拼写:
keyProperty必须对应实体类中的属性名(如id),而不是数据库中的列名(如id)。如果属性名不匹配,回填会失败且不会报错,只是getId()拿不到值。批量插入的回填:
如果你在做口味表的批量插入(
insertBatch),同样可以使用这两个属性。MyBatis 会尝试将生成的一组 ID 对应填入List中的每一个实体对象中。SQL 语句:
不要在 XML 的 SQL 语句里写
id字段及其值。主键字段应该交给数据库自动维护。
💡 生产环境小技巧
在调试时,如果发现 dish.getId() 拿到的是 0 或 null,请第一时间检查两个地方:
- 数据库表结构:
id字段是否勾选了AUTO_INCREMENT。 - MyBatis XML:是否漏写了
useGeneratedKeys="true"。
1.3 菜品分页查询
- 传递数据采用了DishPageQueryDTO,返回数据时使用了DishVO,涉及到了菜品表和分类表的多表查询。
- 在业务上,因为查询的是菜品,不管菜品符不符合连接条件,都要返回,采用了左外连接
1.4 删除菜品
当该菜品处于在售状态,不允许删除
可以一次删除一个或多个菜品
删除菜品后,关联的口味数据也要删除
被套餐关联的菜品不能删除,需要先在套餐中移除菜品,才可删除
接口设计成一次删除多个菜品的接口,把参数放进集合中传递过来,遍历集合,删除菜品及关联的数据,需要开启事务
接口:DELETE /admin/dish/ids=1,2,3
参数类型:String,可以直接使用SpringMVC的@RequestParam注解,自动将参数映射成List
<Long>实际开发中遇到的问题:
- 错误考虑了在Java代码中使用遍历一条一条的删除数据,没有想到SQL的批量删除
<foreach>+ in()
- 错误考虑了在Java代码中使用遍历一条一条的删除数据,没有想到SQL的批量删除
MyBatis <foreach> 标签的使用
案例:
xml<delete id="deleteByIds"> delete from dish where id in <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach> </delete>sql批量删除 DELETE FROM dish WHERE id IN (1, 2, 3)xml<insert id="insertBatch"> insert into dish_flavor (dish_id, name, value) values <foreach collection="flavors" item="f" separator=","> (#{f.dishId}, #{f.name}, #{f.value}) </foreach> </insert>sql批量插入 INSERT INTO dish_flavor (...) VALUES (id, name, val), (id, name, val)...
1.5 修改菜品
需要查询回显,根据ID查询菜品接口,查询菜品分类接口(已实现),图片上传接口(已实现),修改菜品,四个接口
查询菜品回显时,封装成DishVO对象返回,其中的flavors字段是个集合,需要连表查询再映射,这里如何解决这个映射的问题?
这里视频中的方法是分步查询,先查除口味之外的数据,用到 dish 和 category 两个表进行连表查询;之后再查菜品的口味数据,最后将所有的数据封装到一个DishVO对象中返回
修改菜品涉及到dish表,dish_flavor表,setmeal_dish表,需要事务
对于dish_flavor表,由于数据比较复杂,细致调整很麻烦,所以我们拆分为两步操作:删掉原数据,再添加新数据。在添加新数据的时候,我们需要获取菜品的ID,前端并没有将该ID传过来。
对于setmeal_dish表,我们选择同步更新,保持数据一致性,对于这种表,是根据实际业务来决定如何处理的